/*
 * Copyright (c) 2009, Magnus Robertsson. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package se.jayway.javafxbinder.application;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;

import se.jayway.javafxbinder.model.TypeInfo;
import se.jayway.javafxbinder.util.ClassFile;
import se.jayway.javafxbinder.util.ClassNameFilter;
import se.jayway.javafxbinder.util.ClassPath;

/**
 * Generates adapter code between JavaFx and Java fields to provide seamless
 * bind functionality in JavaFx. Currently all JavaBean properties for
 * a POJO will be generated as JavaFX properties.
 *
 * In the future we could use different strategies to locate the properties
 * that should be adapted:
 * <ul>
 * <li>Annotation - Only fields with JavaFxProperty annotation.</li>
 * <li>AspectJ pointcut expressions - Fields matching one or more AspectJ
 *                                    pointcut expression.</li>
 * </ul>
 *
 * @author Magnus Robertsson
 */
public class Main {

	private final Arguments arguments;
	private VelocityEngine engine;
	private Template template;

	public Main(Arguments arguments) {
		this.arguments = arguments;
		init();
	}

	protected void init() {
		try {
			engine = new VelocityEngine();
			Properties props = new Properties();
			//props.setProperty("runtime.log", "velocity.log");
			props.setProperty("resource.loader", "class");
			props.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
			engine.init(props);
			template = engine.getTemplate("javafxadapter.vm");
		} catch (Exception e) {
			throw new IllegalStateException("Unexpected internal error", e);
		}
	}
	
    /**
     * Runs the application using the following arguments:
     * <ul>
     * <li>
     * -classpath - The classpath to search Java types in.
     * </li>
     * <li>
     * -includes - Used to filter classes from the classpath. Default is all
     * classes.
     * </li>
     * <li>
     * -excludes - Filters classes from the classpath. Default is nothing.
     * </li>
     * <li>
     * -package - The package for the JavaFx classes. Default is the same as for
     * the Java classes.
     * </li>
     * <li>
     * -out - The output directory.
     * </li>
     * </ul>
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
    	try {
    		Arguments arguments = Arguments.parseArguments(args);
            Main application = new Main(arguments);
            application.generateJavaFXSourceFiles();
    	} catch (IllegalArgumentException e) {
    		printUsageAndExit(e);
    	}
    }

    private static void printUsageAndExit(IllegalArgumentException e) {
    	System.out.println(e.getMessage());
    	System.out.println();
    	System.out.println("Usage: " + Main.class.getName() + " -classpath <classpath> -includes <regexp> -excludes <regexp> -out <dir>");
    	System.exit(-1);
	}

	private void generateJavaFXSourceFiles() {
        for (ClassFile classFile : arguments.getClassFiles()) {
        	if (!classFile.isAnonymousClass() && !classFile.isMemberClass()) {
	        	generateJavaFXSourceFile(classFile);
        	}
        }
    }

    /**
     * Generates a JavaFX adapter source file for the corresponding Java
     * class file.
     * 
     * @param classFile The class file to be used as template.
     */
	private void generateJavaFXSourceFile(ClassFile classFile) {
		Writer writer = null;
        try {
        	TypeInfo type = new TypeInfo(classFile.loadClass());
            File outputFile = getOutputFile(type);
            writer = new FileWriter(outputFile);
            template.merge(assembleVelocityContext(type), writer);
            System.out.println("Wrote JavaFX adapter to " + outputFile.getAbsolutePath());
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} finally {
			if (writer != null) {
	            try {
					writer.close();
				} catch (IOException e) {
					// Nothing to do
				}
			}
		}
	}
	
	private Context assembleVelocityContext(TypeInfo type) {
    	VelocityContext context = new VelocityContext();
        context.put("type", type);
        return context;
	}

	private File getOutputFile(TypeInfo type) throws IOException {
		String packageFilePath = type.getPackageName().replace('.', File.separatorChar);
		File packageDir = new File(arguments.getOutputDir(), packageFilePath);
		if (!packageDir.exists()) {
			if (!packageDir.mkdirs()) {
				throw new IOException("Could not create directory " + packageDir);
			}
		} else if (!packageDir.isDirectory()) {
			throw new IOException("Expected directory " + packageDir + " is a file that exists");
		}
		return new File(packageDir, "Jfx" + type.getSimpleName() + ".fx");
	}
}

class Arguments {
	private File outDir;
	private ClassPath classPath;
	private List<String> includes = new ArrayList<String>();
	private List<String> excludes = new ArrayList<String>();

    public File getOutputDir() {
        return outDir;
    }

    public Collection<ClassFile> getClassFiles() {
    	try {
	        return classPath.findClassFiles(new ClassNameFilter() {
				public boolean matches(String className) {
					return matches(className, includes) && !matches(className, excludes);
				}

				private boolean matches(String className, List<String> expressions) {
					for (String expression : expressions) {
						if (className.matches(expression)) {
							return true;
						}
					}
					return false;
				}
	        });
    	} catch (IOException e) {
    		throw new RuntimeException("Could not load one or more classes", e);
    	}
    }

    private void verify() {
    	if (outDir == null) {
    		throw new IllegalArgumentException("No output directory specified");
    	}

    	if (classPath == null) {
    		throw new IllegalArgumentException("No classpath specified");
    	}
    	
    	if (includes.isEmpty()) {
    		includes.add("*");
    	}
    }

    public static Arguments parseArguments(String[] args) {
        Arguments arguments = new Arguments();
        String option = null;
        for (String arg : args) {
            if (option == null) {
                option = arg;
            } else {
            	arguments.parseOption(option, arg);
                option = null;
            }
        }
        if (option != null) {
        	throw new IllegalArgumentException("Option " + option + " need an argument");
        }
        arguments.verify();
        return arguments;
    }
    
    private void parseOption(String option, String param) {
        if (option.equals("-out")) {
        	parseOutputDirectory(param);
        } else if (option.equals("-classpath")) {
        	parseClassPath(param);
        } else if (option.equals("-includes")) {
        	includes.add(param);
        } else if (option.equals("-excludes")) {
        	excludes.add(param);
        } else {
            throw new IllegalArgumentException("Illegal option: " + option);
        }
    }

	private void parseClassPath(String param) {
		try {
			classPath = ClassPath.parseClassPath(param);
		} catch (MalformedURLException e) {
			throw new IllegalArgumentException("Illegal classpath: " + e.getMessage());
		}
	}

	private void parseOutputDirectory(String param) {
		if (outDir != null) {
			throw new IllegalArgumentException("Output directory already specified");
		}
		outDir = new File(param);
		if (!outDir.exists()) {
			if (!outDir.mkdirs()) {
				throw new IllegalArgumentException("Could not create output directory " + outDir.getAbsolutePath());
			}
		}
		if (!outDir.isDirectory()) {
			throw new IllegalArgumentException("Output directory " + param + " is not a directory");
		}
	}
}